<!DOCTYPE html>
<meta charset=utf-8>
<title>Test clamping logic of element-based scroll offset for scroll timeline.</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="testcommon.js"></script>

<style>
/*
 * Overflow hidden prevents user scroll including mouse wheel; however, the
 * element is still a scrollable container and can be scrolled programmatically.
 * Removing the visible scrollbars in this manner simplifies the position
 * calculations in the text.
 */
.scroller {
  overflow: hidden;
  height: 500px;
  width: 500px;
  will-change: transform;
}

.contents {
  height: 1200px;
  width: 1200px;
  position: relative;
}

.vertical #target {
  background: blue;
  border-top: 0px solid pink;
  border-bottom: 0px solid pink;
  box-sizing: border-box;
  position: absolute;
  width: 100%;
  top: var(--start-position);
  height: calc(var(--end-position) - var(--start-position));
}

.horizontal #target {
  background: blue;
  border-left: 0px solid pink;
  border-right: 0px solid pink;
  box-sizing: border-box;
  position: absolute;
  height: 100%;
  left: var(--start-position);
  width: calc(var(--end-position) - var(--start-position));
}
</style>
<div id="log"></div>
<script>
  'use strict';

  function createScrollerWithTarget(test, config) {
    const orientationClass = config.orientation;
    const positions = `
    --start-position: ${config.startElementPosition};
    --end-position: ${config.endElementPosition};`

    var scroller = createDiv(test);
    scroller.innerHTML =
     `<div class='contents' style="${positions}">
        <div id='target'></div>
      </div>`;
    scroller.classList.add('scroller');
    scroller.classList.add(orientationClass);

    return scroller;
  }

  async function createScrollAnimationTest(description, config) {
    promise_test(async t => {
      const scroller = createScrollerWithTarget(t, config);
      t.add_cleanup(() => scroller.remove());

      const target = scroller.querySelector("#target");

      // Force layout before creating the scroll timeline to ensure the correct
      // scroll range.
      target.offsetHeight;

      const timeline = createScrollTimeline(t, {
        scrollSource: scroller,
        orientation: config.orientation,
        timeRange: 1000,
        fill: 'both',
        scrollOffsets: [{target: target, edge: 'end', ...config.start},
                        {target: target, edge:'start', ...config.end }]
      });

      // Wait for new animation frame which allows the timeline to compute new
      // current time.
      await waitForNextFrame();

      const animation = createScrollLinkedAnimation(t, timeline);
      const timeRange = animation.timeline.timeRange;

      // Verify initial start and current times in Idle state.
      assert_equals(animation.currentTime, null,
        "The current time is null in Idle state.");
      assert_equals(animation.startTime, null,
        "The start time is null in Idle state.");

      animation.play();
      assert_true(animation.pending, "Animation is in pending state.");
      // Verify initial start and current times in Pending state.
      assert_times_equal(animation.currentTime, 0,
        "The current time is zero in Pending state.");
      assert_equals(animation.startTime, 0,
        "The start time is zero in Pending state.");

      await animation.ready;
      // Verify initial start and current times in Playing state.
      assert_times_equal(animation.currentTime, 0,
        "The current time is zero in Playing state.");
      assert_times_equal(animation.startTime, 0,
        "The start time is zero in Playing state.");

      // Now do some scrolling and make sure that the Animation current time is
      // correct.
      if (config.orientation == 'vertical') {
        scroller.scrollTo({top: config.scrollTo});
        assert_equals(scroller.scrollTop, config.scrollTo);
      } else {
        scroller.scrollTo({left: config.scrollTo});
        assert_equals(scroller.scrollLeft, config.scrollTo);
      }

      await waitForNextFrame();

      assert_times_equal(animation.timeline.currentTime, config.expectedCurrentTime,
        "The timeline current time corresponds to the scroll position of the scroller.");
      assert_times_equal(animation.currentTime, config.expectedCurrentTime,
        "The animation current time corresponds to the scroll position of the scroller.");
      assert_times_equal(
        animation.effect.getComputedTiming().localTime,
        config.expectedCurrentTime,
        'Effect local time corresponds to the scroll position of the scroller.');
    }, description);
  }

  // We have no scrollbar and the scroller is symmetric on x & y axis so this
  // static value is axis and platform agnostic.
  const scroll_max = 700;

  // For this test we setup a single target, and scroll timeline in a way that
  // our animation runs from when target enters the scroll port until it fully
  // exits it. Then we create various edgecase scenarios to see the clamping
  // logic.
  //
  // Scroller has 500px heights with 1200px content which translates to
  // 0 < scroll < 700px
  //
  //  +----------+  ^
  //  |          |  |
  //  | Scroller |  |
  //  |          |  | scrollRange
  //  |          |  |
  //  +----------+  |     +--+
  //      |TT|      |     |TT|
  //      +--+      v +----------+
  //                  |          |
  //                  | Scroller |
  //                  |          |
  //                  |          |
  //                  +----------+
  //
  // For each test the expected timeline start/end is in the comment to help
  // with the verification.
  const tests = {
    // offsets: [0, 600]
    "no clamping is expected": {
      startElementPosition: '500px',
      endElementPosition: '600px',
      scrollTo: 300,
      expectedCurrentTime: 500,
    },
    // offsets: [0, 600]
    "start is visible at zero offset and should get clamped": {
      startElementPosition: '400px',
      endElementPosition: '600px',
      scrollTo: 300,
      expectedCurrentTime: 500,
    },

    // offsets: [0, scroll_max]
    "end is not reachable and should be clamped": {
      startElementPosition: '500px',
      endElementPosition: '800px',
      scrollTo: scroll_max / 2,
      expectedCurrentTime: 500,
    },

    // offsets: [0, scroll_max]
    "both start and end are clamped": {
      startElementPosition: '400px',
      endElementPosition: '800px',
      scrollTo: scroll_max / 2,
      expectedCurrentTime: 500,
    },
  };

  for (let orientation of ['vertical', 'horizontal']) {
    for (let testName in tests) {
      const description = `Animation start and current times are correct given
          element-based offsets for orienation ${orientation} and ${testName}.`;
      const config = tests[testName];
      config.orientation = orientation;
      createScrollAnimationTest(description, config);
    }
  }
</script>
